﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="FileChangeTrackingManager.cs" company="" />
// <summary>
//   Tracks changes in registered files.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Utilities.FileChangesTracking
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;

    /// <summary>
    /// The types of changes that are monitored by the file tracking mechanism.
    /// </summary>
    public enum FileChangeType
    {
        FileChanged,
        FileDeleted,
        FileRenamed
    }

    /// <summary>
    /// Tracks changes in registered files.
    /// </summary>
    public static class FileChangeTrackingManager
    {
        /// <summary>
        /// Maps the directory name to its file system watcher.
        /// </summary>
        private static readonly Dictionary<string, FileChangeSubscription> s_watchers = new Dictionary<string, FileChangeSubscription>();

        /// <summary>
        /// Maintains the list of changed files between calls to <see cref="GetLastChanges"/>().
        /// </summary>
        private static readonly HashSet<FileChangeNotification> s_changedFileEvents = new HashSet<FileChangeNotification>();

        /// <summary>
        /// A synchronzation object for <see cref="s_changedFileEvents"/>.
        /// </summary>
        private static readonly object s_changesListSync = new object();

        #region Change handlers

        /// <summary>
        /// A change handler for a file-change.
        /// </summary>
        private static readonly FileSystemEventHandler s_fileChangedHandler =
            (sender, args) =>
            {
                lock (s_changesListSync)
                {
                    s_changedFileEvents.Add(new FileChangeNotification(args.FullPath, FileChangeType.FileChanged));
                }
            };

        /// <summary>
        /// A change handler for a file-deletion.
        /// </summary>
        private static readonly FileSystemEventHandler s_fileDeletedHandler =
            (sender, args) =>
            {
                lock (s_changesListSync)
                {
                    s_changedFileEvents.Add(new FileChangeNotification(args.FullPath, FileChangeType.FileDeleted));
                }
            };

        /// <summary>
        /// A change handler for a file-renaming.
        /// </summary>
        private static readonly RenamedEventHandler s_fileRenamedHandler =
            (sender, args) =>
            {
                lock (s_changesListSync)
                {
                    s_changedFileEvents.Add(new FileChangeNotification(args.OldFullPath, FileChangeType.FileRenamed, args.FullPath));
                }
            };

        #endregion

        /// <summary>
        /// Registers a file for watching. Many handlers can be registered to handle the same file change.
        /// </summary>
        /// <param name="filePath">The name of the file to watch. Has to be a file name.</param>
        /// <param name="changeHandler">The handler to handle a change in the given file.</param>
        /// <returns>The GUID associated with the given change-handler.</returns>
        public static Guid RegisterFileHandler(string filePath, FileChangeHandler changeHandler)
        {
            if (string.IsNullOrEmpty(Path.GetFileName(filePath)))
            {
                throw new ArgumentException("Given file-path does not correspond to a file.");
            }

            string fileName = Path.GetFileName(filePath);
            string fileFolder = Path.GetDirectoryName(filePath);

            if (!s_watchers.ContainsKey(fileFolder))
            {
                // 'FileSystemWatcher' accepts only directory names:
                var newWatcher = new FileSystemWatcher(Path.GetDirectoryName(filePath))
                {
                    IncludeSubdirectories = false,
                    Filter = string.Empty,
                    NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName,
                    EnableRaisingEvents = false,
                };

                newWatcher.Changed += s_fileChangedHandler;
                newWatcher.Deleted += s_fileDeletedHandler;
                newWatcher.Renamed += s_fileRenamedHandler;
                newWatcher.EnableRaisingEvents = true;
                s_watchers.Add(fileFolder, new FileChangeSubscription(newWatcher));
            }

            var newGuid = Guid.NewGuid();
            s_watchers[fileFolder].Handlers.Add(newGuid, new FileChangeHandlerInfo(fileName, changeHandler));

            return newGuid;
        }

        /// <summary>
        /// Unsubscribe a handler from the list of handlers associated with a specific file watcher.
        /// If this is the last of the handlers - the watcher is removed as well.
        /// </summary>
        /// <param name="filePath">The file path identifying the watcher.</param>
        /// <param name="handlerId">The ID given to the handler to remove at the time of subscription.</param>
        public static void UnsubscribeChangeHandler(string filePath, Guid handlerId)
        {
            if (string.IsNullOrEmpty(Path.GetFileName(filePath)))
            {
                throw new ArgumentException("Given file-path does not correspond to a file.");
            }

            var subscription = s_watchers[Path.GetDirectoryName(filePath)];

            subscription.Handlers.Remove(handlerId);
            if (!subscription.Handlers.Any())
            {
                RemoveWathcer(filePath);
            }
        }

        /// <summary>
        /// Stop watching for the given file, and remove the watcher from the maintained list.
        /// </summary>
        /// <param name="filePath">The path by which to identify the file-watcher to unsubscribe.</param>
        public static void RemoveWathcer(string filePath)
        {
            string fileFolder = Path.GetDirectoryName(filePath);

            UnsubscribeWatcher(fileFolder);
            s_watchers.Remove(fileFolder);
        }

        /// <summary>
        /// Stop watching on all registered file-paths and remove them from subscription list.
        /// </summary>
        public static void RemoveAllWatchers()
        {
            foreach (var watcher in s_watchers)
            {
                UnsubscribeWatcher(watcher.Key);
            }

            s_watchers.Clear();
        }

        /// <summary>
        /// Unsubscribes a file-watcher from the list of files watched by the file-system.
        /// </summary>
        /// <param name="folderPath">The path by which to identify the file-watcher to unsubscribe.</param>
        private static void UnsubscribeWatcher(string folderPath)
        {
            if (!s_watchers.ContainsKey(folderPath))
            {
                throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Trying to remove a non registered watcher for path {0}", folderPath));
            }

            var watcher = s_watchers[folderPath].Watcher;

            watcher.EnableRaisingEvents = false;
            watcher.Changed -= s_fileChangedHandler;
            watcher.Deleted -= s_fileDeletedHandler;
            watcher.Renamed -= s_fileRenamedHandler;
        }

        /// <summary>
        /// Retrieve the list of changed files since last call to this method.
        /// </summary>
        /// <returns>List of unique changes of files.</returns>
        public static List<FileChangeNotification> GetLastChanges()
        {
            // Get a snapshot of the enlisted changes:
            lock (s_changesListSync)
            {
                var listSnapShot = s_changedFileEvents.ToList();

                s_changedFileEvents.Clear();

                return listSnapShot;
            }
        }

        /// <summary>
        /// Read the last collected notifications and invoke relevant subscriptions. Then the notifications are cleared.
        /// </summary>
        public static void NotifySubscribers()
        {
            foreach (var notification in GetLastChanges())
            {
                string fileFolder = Path.GetDirectoryName(notification.FilePath);
                string fileName = Path.GetFileName(notification.FilePath);
                var subscription = s_watchers[fileFolder];

                foreach (var subscriber in subscription.Handlers.Values.Where(handlerInfo => handlerInfo.FileName.Equals(fileName)))
                {
                    subscriber.Handler(notification.ChangeEssence, notification.FilePath, notification.NewPath);
                }
            }
        }
    }
}
